package net.fxug.hokuriku.chocolat.dao
{
    import flash.data.SQLConnection;
    import flash.data.SQLResult;
    import flash.data.SQLStatement;
    import flash.errors.IllegalOperationError;
    import flash.events.EventDispatcher;
    import flash.events.SQLErrorEvent;
    import flash.events.SQLEvent;
    import flash.filesystem.File;

    import mx.logging.ILogger;
    import mx.logging.Log;

    import net.fxug.hokuriku.chocolat.dao.contents.SQLQuery;
    import net.fxug.hokuriku.chocolat.errors.AlreadyConnectedError;
    import net.fxug.hokuriku.chocolat.events.SQLVoteEvent;
    import net.fxug.hokuriku.chocolat.utils.DatetimeUtil;

    /**
     * セッション登録イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.INSERT_SESSION_EVENT
     */
    [Event(name="insertSessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * セッション取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_SESSION_EVENT
     */
    [Event(name="selectSessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * 投票登録イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.INSERT_VOTE_EVENT
     */
    [Event(name="insertVoteEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * 投票取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_VOTE_EVENT
     */
    [Event(name="selectVoteEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * セッションID指定時の投票情報取得イベント
     * @eventType net.fxug.hokuriku.chocolat.events.SQLVoteEvent.SELECT_VOTE_BY_SESSION_EVENT
     */
    [Event(name="selectVoteBySessionEvent", type="net.fxug.hokuriku.chocolat.events.SQLVoteEvent")]
    /**
     * DB接続イベント
     * @eventType flash.events.SQLEvent.OPEN
     */
    [Event(name="open", type="flash.events.SQLEvent")]
    /**
     * 取得失敗イベント
     * @eventType flash.events.SQLErrorEvent.ERROR
     */
    [Event(name="error", type="flash.events.SQLErrorEvent")]

    /**
     * データベースへの接続を管理するためのマネージャークラス
     * このクラスはシングルトンで動作する。
     *
     * FIXME あぁ、やっぱりこの実装だと面倒だorz...せめてFunctionを渡すような実装にしておけばよかったぽ。
     * FIXME 現行の実装では、1クエリ1ステートメントの構成になっており、トランザクションはデフォルトの設定で実行される。]
     * TODO 値の検証が実装されていない。特にプライマリキー関連。DBでは制約がかれないため注意。
     *
     * このクラスで実行されるDB関連処理は非同期で実行される。
     * @author hikaruworld
     */
    public class DatabaseManager extends EventDispatcher
    {
        /** 自身を表す一意なインスタンス */
        private static var instance:DatabaseManager;

        /** ロガー */
        private var logger:ILogger;

        /**
         * SQL接続を行うSQLConnection
         * TODO 必要に応じてこのSQLConnectionをキュー化してもよい
         */
        private var conn:SQLConnection;

        /** セッション内容取得に利用されるSQLStatement */
        private var findSessionStmt:SQLStatement;
        /** セッション内容登録に利用されるSQLStatement */
        private var insertSessionStmt:SQLStatement;
        /** 投票内容取得に利用されるSQLStatement */
        private var findVoteStmt:SQLStatement;
        /** 投票内容登録に利用されるSQLStatement */
        private var insertVoteStmt:SQLStatement;
        /** セッションIDを指定した投票内容取得のためのSQLStatement */
        private var findVoteBySessionIdStmt:SQLStatement;

        /**
         * 自身の一意なインスタンスを取得する。
         */
        public static function getInstance():DatabaseManager
        {
            if (instance == null)
            {
                instance = new DatabaseManager(new SingletonBlockr());
            }
            return instance;
        }

        /**
         * コンストラクタ
         * @param	blockr Singleton制御用
         */
        public function DatabaseManager(blockr:SingletonBlockr = null)
        {
            if (blockr == null)
            {
                throw new IllegalOperationError("このクラスの外部からのインスタンス化は許可されていません");
            }
            this.init();
        }

        /**
         * 現在の接続状態を返す。
         * @return 接続されいる状態であればtrue
         */
        public function isCoonected():Boolean
        {
            return this.conn == null ? false : this.conn.connected;
        }

        /**
         * セッション情報を登録する。
         */
        public function insertSession(sessionDao:SessionDao):void
        {
            this.insertSessionStmt.clearParameters();
//			this.insertSessionStmt.parameters["sessionId"] = sessionDao.sessionId;
            this.insertSessionStmt.parameters[":startDatetime"] = DatetimeUtil.roundDownByMillsecond(sessionDao.startDatetime);
            this.insertSessionStmt.parameters[":sessionTag"] = sessionDao.sessionTag;
            this.insertSessionStmt.parameters[":title"] = sessionDao.title;
            this.insertSessionStmt.parameters[":atndId"] = sessionDao.atndId;

            logger.info("insert Session[" + sessionDao + "]");
            trace("exec::" + this.insertSessionStmt.text);
            this.insertSessionStmt.execute();
        }

        /**
         * セッション情報を取得する。
         */
        public function selectSession():void
        {
            this.findSessionStmt.clearParameters();
            this.findSessionStmt.itemClass = SessionDao;

            trace("exec::" + this.findSessionStmt.text);
            this.findSessionStmt.execute();
        }

        /**
         * 投票情報を登録する。
         */
        public function insertVote(voteDao:VoteDao):void
        {
            this.insertVoteStmt.clearParameters();
            this.insertVoteStmt.parameters[":datetime"] = DatetimeUtil.roundDownByMillsecond(voteDao.datetime);
            this.insertVoteStmt.parameters[":protocol"] = voteDao.protocol;
            this.insertVoteStmt.parameters[":sessionId"] = voteDao.sessionId;
            this.insertVoteStmt.parameters[":tweet"] = voteDao.tweet;
            this.insertVoteStmt.parameters[":userId"] = voteDao.userId;
            this.insertVoteStmt.parameters[":route"] = voteDao.route;

            logger.info("insert VoteDao[" + voteDao + "]");
            trace("exec::" + this.insertVoteStmt.text);
            this.insertVoteStmt.execute();
        }

//        private var voteArray:Array;
//
//        private var multiInsertVoteStmt:SQLStatement;

        /**
         * 投票情報を登録する。
         * 複数のVote情報を登録する際に利用する。
         * 注意点:このメソッドはSQLStatementを実行時のタイミングで生成するので効率が悪い。
         * FIXME ちゃんとした対応が必要。
         */
        public function insertMultiVote(voteArray:Array):void
        {
            if (voteArray == null || voteArray.length == 0)
            {
                return;
            }
            trace("insertVoteComit...");
            var now:int = 0;
            var limit:int = voteArray.length;
            var multiInsertVoteStmt:SQLStatement = new SQLStatement();

            multiInsertVoteStmt.sqlConnection = this.conn;
            multiInsertVoteStmt.text = SQLQuery.INSERT_VOTE;
            multiInsertVoteStmt.addEventListener(SQLErrorEvent.ERROR, function(sqlError:SQLErrorEvent):void
                {
                    trace(sqlError);
                });
            multiInsertVoteStmt.addEventListener(SQLEvent.RESULT, function(sqlEvent:SQLEvent):void
                {
                    now++;
                    if (now < limit)
                    {
                        internalInsert(multiInsertVoteStmt, now, limit, voteArray);
                    }
                });

            this.internalInsert(multiInsertVoteStmt, now, limit, voteArray);
        }

        private function internalInsert(multiInsertVoteStmt:SQLStatement, now:int, limit:int, voteArray:Array):void
        {
            var voteDao:VoteDao = voteArray[now];
            trace("internalInsert excute by " + now + "::" + voteDao);
            multiInsertVoteStmt.parameters[":datetime"] = DatetimeUtil.roundDownByMillsecond(voteDao.datetime);
            multiInsertVoteStmt.parameters[":protocol"] = voteDao.protocol;
            multiInsertVoteStmt.parameters[":sessionId"] = voteDao.sessionId;
            multiInsertVoteStmt.parameters[":tweet"] = voteDao.tweet;
            multiInsertVoteStmt.parameters[":userId"] = voteDao.userId;
            multiInsertVoteStmt.parameters[":route"] = voteDao.route;
            trace("exec::" + multiInsertVoteStmt.text);
            multiInsertVoteStmt.execute();
        }

//
//        private function sqlStatementInsertResultHandler(event:SQLEvent):void
//        {
//            this.now++;
//            if (now < limit)
//            {
//                internalInsert();
//            }
//        }

        /**
         * 投票情報を取得する
         */
        public function selectVote():void
        {
            this.findVoteStmt.clearParameters();
            this.findVoteStmt.itemClass = VoteDao;

            trace("exec::" + this.findVoteStmt.text);
            this.findVoteStmt.execute();
        }

        /**
         * セッションIDを指定して投票情報を取得する。
         */
        public function findVoteBySessionId(sessionId:int):void
        {
            if (sessionId <= 0)
            {
                throw new ArgumentError("sessionIdに不正な値::" + String(sessionId) + "::が指定されました");
            }
            this.findVoteBySessionIdStmt.clearParameters();
            this.findVoteBySessionIdStmt.itemClass = VoteDao;
            this.findVoteBySessionIdStmt.parameters[":sessionId"] = sessionId;

            trace("exec::" + this.findVoteBySessionIdStmt.text);
            this.findVoteBySessionIdStmt.execute();
        }

        /**
         * 生成したSQLConnectionを利用してDBへ接続を行う。
         * @param trueの場合に既存のDBを削除して新規作成する。
         */
        public function connect(init:Boolean = false):void
        {
            // 既に接続されている場合はエラーを返す。
            if (this.conn != null && this.conn.connected)
            {
                throw new AlreadyConnectedError("対象DBには既に接続しています");
            }
            // DBの存在チェックを行い、存在しなかった場合はローカルテンプレートからコピーする
            var dbFile:File = File.applicationStorageDirectory.resolvePath("soraai.db");
            logger.debug("dbFile.exists : " + dbFile.exists);
            logger.debug("dbFile.netivePath : " + dbFile.nativePath);

            // 既にDBが存在しており、かつ初期化が必要な場合 -> 既存のDBを削除(退避)させて、新規作成する。
            // 既にDBが存在しているが、初期化が不要な場合 -> 既存のDBに接続(退避されたDBしか存在しない場合は復旧する)
            // FIXME 必要があれば最も直近のDBをリネームして復旧させるような処理が必要。
            // DBが存在していないのにも関わらず、初期化が必要な場合 -> 単純に新規作成する。
            // DBが存在しておらず、初期化が不要な場合 -> 単純に新規作成する。

            // 初期化が必要でかつ、既存のDBが存在しない場合
            if (init)
            {
                if (dbFile.exists)
                {
                    dbFile.deleteFile();
                }
                this.createDatabase(dbFile);
            }
            // 初期化が不要の場合
            else
            {
                if (!dbFile.exists)
                {
                    //FIXME ログでワーニングを出すかErrorをthrowする。 
                    this.createDatabase(dbFile);
                }
                else
                {
                    this.verifyDatabase(dbFile);
                }
            }
            // 非同期モードで接続
            this.conn.addEventListener(SQLEvent.OPEN, conn_openHandler);
            this.conn.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
            this.conn.openAsync(dbFile);
        }

        /**
         * DBに非同期モードで正常に接続できた場合に発行される。
         */
        private function conn_openHandler(sqlEvent:SQLEvent):void
        {
            this.conn.removeEventListener(SQLEvent.OPEN, conn_openHandler);
            logger.debug("DBとのコネクションが確立されました。");
            // TODO とりあえずプロキシする
            this.dispatchEvent(sqlEvent);
        }

        /**
         * DBを作成する。
         */
        private function createDatabase(toFile:File):void
        {
            // 削除後新規作成する。
            //Application内部からテンプレートを取得
            var tempFile:File = File.applicationDirectory.resolvePath("assets/db/soraai.db");
            // 添付ファイルがもし存在しない場合はSQLを流して接続する。
            if (!tempFile.exists)
            {
                logger.info("テンプレートファイルが参照できなかったため、CREATE文を使ってテーブルを作成します。");
                throw new IllegalOperationError("実装先送り");
            }
            // 同期モードで上書きコピーする。
            try
            {
                tempFile.copyTo(toFile, true);
            }
            catch (error:Error)
            {
                // TODO カスタム例外がいるな...
                throw new Error(error.message, error.errorID);
            }
            logger.debug("同期モードでを用いてテンプレートからのファイルコピーが完了しました.");
        }

        /**
         * DBのテーブル構成を検証する。
         */
        [Deprecated("このメソッドはなにも実装されていません。")]
        private function verifyDatabase(file:File):void
        {
            //TODO 検証ロジック
        }

        /**
         * DB接続を破棄する。
         * @param 名称を変更して保存する。
         */
        public function destory(renameSave:Boolean = false):void
        {
//			this.findSessionStmt.;
//			this.findVoteStmt;
//			this.insertSessionStmt;
//			this.insertVoteStmt;
            this.conn.addEventListener(SQLEvent.RESULT, conn_closeEventHandler);
            this.conn.close();
        }

        private function conn_closeEventHandler(event:SQLEvent):void
        {
            this.conn.removeEventListener(SQLEvent.RESULT, conn_closeEventHandler);
            logger.info("DBを正常にcloseしました.");
        }

        /**
         * 初期化処理.
         * 各種SQLのQueryの接続及び初期化を行う.
         * @param create trueの場合に新規にDBを生成する。
         */
        private function init():void
        {
            logger = Log.getLogger("net.fxug.hokuriku.chocolat.dao.DatabaseManager");
            logger.info("initlized DatabaseController.....");

            // SQLConnectionクラスのインスタンス化
            this.conn = new SQLConnection();
            //this.conn.addEventListener(SQLEvent.OPEN, sqlOpenHandler);
            this.conn.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            // Statementの初期化を行う。
            // セッション系
            logger.debug("Create Statements SELECT SESSION...");
            this.findSessionStmt = new SQLStatement();
            this.findSessionStmt.sqlConnection = this.conn;
            this.findSessionStmt.text = SQLQuery.SELECT_SESSION;
            this.findSessionStmt.itemClass = SessionDao;
            this.findSessionStmt.addEventListener(SQLEvent.RESULT, findSessionStmt_resultHandler);
            this.findSessionStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
            logger.debug("Create Statements INSERT SESSION...");
            this.insertSessionStmt = new SQLStatement();
            this.insertSessionStmt.sqlConnection = this.conn;
            this.insertSessionStmt.text = SQLQuery.INSERT_SESSION;
            this.insertSessionStmt.addEventListener(SQLEvent.RESULT, insertSessionStmt_resultHandler);
            this.insertSessionStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            // 投票系
            logger.debug("Create Statements SELECT VOTE...");
            this.findVoteStmt = new SQLStatement();
            this.findVoteStmt.sqlConnection = this.conn;
            this.findVoteStmt.text = SQLQuery.SELECT_VOTE;
            this.findVoteStmt.itemClass = VoteDao;
            this.findVoteStmt.addEventListener(SQLEvent.RESULT, findVoteStmt_resultHandler);
            this.findVoteStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            logger.debug("Create Statements INSERT VOTE...");
            this.insertVoteStmt = new SQLStatement();
            this.insertVoteStmt.sqlConnection = this.conn;
            this.insertVoteStmt.text = SQLQuery.INSERT_VOTE;
            this.insertVoteStmt.addEventListener(SQLEvent.RESULT, insertVoteStmt_resultHandler);
            this.insertVoteStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);

            logger.debug("Create Statements SELECT VOTE BY SESSION_ID...");
            this.findVoteBySessionIdStmt = new SQLStatement();
            this.findVoteBySessionIdStmt.sqlConnection = this.conn;
            this.findVoteBySessionIdStmt.text = SQLQuery.SELECT_VOTE_BY_SESSION_ID;
            this.findVoteBySessionIdStmt.itemClass = VoteDao;
            this.findVoteBySessionIdStmt.addEventListener(SQLEvent.RESULT, findVoteBySessionIdStmt_resultHandler);
            this.findVoteBySessionIdStmt.addEventListener(SQLErrorEvent.ERROR, sqlErrorHandler);
//            // DBへ非同期モードで接続
//            this.connect();
        }

        /**
         * @private
         * セッション情報登録成功時のイベントハンドラ
         */
        private function insertSessionStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.insertSessionStmt.getResult();

            logger.info("insertSessionStmt_resultHandler::" + sqlResult);

            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.INSERT_SESSION_EVENT, sqlResult.lastInsertRowID, resultEvent.bubbles, resultEvent.cancelable);
            this.dispatchEvent(retEvent);
        }

        /**
         * @private
         * セッション情報取得成功時のイベントハンドラ
         */
        private function findSessionStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_SESSION_EVENT, this.findSessionStmt.getResult().data, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

        /**
         * @private
         * 投票登録成功時のイベントハンドラ
         * TODO VoteIdをDBに設定できるようにしてそれを返すようにするかも...
         */
        private function insertVoteStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var sqlResult:SQLResult = this.insertVoteStmt.getResult();

            logger.info("insertVoteStmt_resultHandler::" + sqlResult);

            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.INSERT_VOTE_EVENT, sqlResult.lastInsertRowID, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

        /**
         * @private
         * 投票データ取得成功時のイベントハンドラ
         */
        private function findVoteStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_VOTE_EVENT, this.findVoteStmt.getResult().data, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

        /**
         *
         */
        private function findVoteBySessionIdStmt_resultHandler(resultEvent:SQLEvent):void
        {
            var retEvent:SQLVoteEvent = new SQLVoteEvent(SQLVoteEvent.SELECT_VOTE_BY_SESSIONID_EVENT, this.findVoteBySessionIdStmt.getResult().data, resultEvent.bubbles, resultEvent.cancelable);

            this.dispatchEvent(retEvent);
        }

        /**
         * DBの初期化が完了した際に発生するイベントハンドラ。
         *
         * XXX デフォルトではこのイベントハンドラが発生することはないはず。基本的にはコピーを行うため。
         * TODO 未実装
         * @param	e
         */
        private function initDBHandler(e:SQLEvent):void
        {
            this.logger.info("init DB Table created...");
        }

        /**
         * SQLのOpenに失敗した場合に表示されるイベントハンドラ
         * @param	e SQLErrorEvent
         */
        private function sqlErrorHandler(event:SQLErrorEvent):void
        {
            this.logger.error("Error message:", event.error.message, " Details:", event.error.details);
            this.dispatchEvent(event);
        }
    }
}

/** インスタンス生成抑止用 */
class SingletonBlockr
{
}
